#!/bin/sh

# This file defines the edmail mail user agent.

# Copyright (C) 2017-2018 mlaine@sdfeu.org

# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:

# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#-----------------------------------------------------------------------

mon2num () {
    case "$mon" in
        'Jan') mon=1  ;;
        'Feb') mon=2  ;;
        'Mar') mon=3  ;;
        'Apr') mon=4  ;;
        'May') mon=5  ;;
        'Jun') mon=6  ;;
        'Jul') mon=7  ;;
        'Aug') mon=8  ;;
        'Sep') mon=9  ;;
        'Oct') mon=10 ;;
        'Nov') mon=11 ;;
        'Dec') mon=12 ;;
    esac
}

pad () {
    case "$1" in
        [0-9]) printf '0%s\n' "$1" ;;
        *)     printf '%s\n'  "$1" ;;
    esac
}

parsedate () {
    date="$(awk '/^Date: /{print;exit}' "$file" | cut -d' ' -f2-)"
    set -- $(printf '%s\n' "${date#*, }")
    day="${1#0}"; mon="$2"; year="$3"; time="${4%:*}"; tz="${5#*[+-]}"
    tz="$(printf '%s' "$tz" | awk '{print substr ($0,1,4)}')"
    hrs="${time%:*}"; hrs="${hrs#0}"
    min="${time#*:}"; min="${min#0}"
    tzhrs="${tz%??*}"; tzhrs="${tzhrs#0}"
    tzmin="${tz#*??}"; tzmin="${tzmin#0}"
    mon2num
    case "$5" in
        '+0000'|'-0000'|'GMT')
            true ;;
        '+'*)
            hrs="$((hrs-tzhrs))"
            min="$((min-tzmin))"
            test "$min" -lt 00 && min="$((min+60))" && hrs="$((hrs-1))"
            test "$hrs" -lt 00 && hrs="$((hrs+24))" && day="$((day-1))"
            case "$mon" in
                3)
                    test "$day" -lt 1 && day=28 && mon="$((mon-1))" ;;
                5|7|10|12)
                    test "$day" -lt 1 && day=30 && mon="$((mon-1))" ;;
                *)
                    test "$day" -lt 1 && day=31 && mon="$((mon-1))" ;;
            esac ;;
        '-'*)
            hrs="$((hrs+tzhrs))"
            min="$((min+tzmin))"
            test "$min" -gt 59 && min="$((min%60))" && hrs="$((hrs+1))"
            test "$hrs" -gt 23 && hrs="$((hrs%24))" && day="$((day+1))"
            case "$mon" in
                2)
                    test "$day" -gt 28 && day=1 && mon="$((mon+1))" ;;
                4|6|9|11)
                    test "$day" -gt 30 && day=1 && mon="$((mon+1))" ;;
                *)
                    test "$day" -gt 31 && day=1 && mon="$((mon+1))" ;;
            esac ;;
        *)
            hrs='??'; min='??' ;;
    esac
    test "$mon" -gt 12 && mon=1  && year="$((year+1))"
    test "$mon" -lt 1  && mon=12 && year="$((year-1))"
    mon="$(pad "$mon")"; day="$(pad "$day")"
    hrs="$(pad "$hrs")"; min="$(pad "$min")"
}

awkmatch () {
    if [ -n "$inverse" ]; then
        awk -v p="$pat" 'BEGIN{s=0}$0~p{s=1;exit}END{exit s}' "$file"
    else
        awk -v p="$pat" 'BEGIN{s=1}$0~p{s=0;exit}END{exit s}' "$file"
    fi
}

update () {
    if [ -n "$filter" ]; then
        true > "$templist"
        find "$dir" -type f |
        while read -r file; do
            if awkmatch; then
                printf '%s\n' "$file" >> "$templist"
            fi
        done
    else
        find "$dir" -type f > "$templist"
    fi
    if ! cmp -s "$templist" "$filelist"; then
        cp "$templist" "$filelist"
        true > "$statlist"
        while read -r file; do
            parsedate
            set -- $(wc < "$file")
            set -- "$file" "$1" "$3" "$year" "$mon" "$day" "$hrs" "$min"
            printf '%s\n' "$*"  >> "$statlist"
        done < "$filelist"
        n="$(awk 'END{print NR}' "$filelist")"
    fi
    if [ -n "$bysize" ]; then
        sort -k 3,3nr -o "$statlist" "$statlist"
    elif ! cmp -s "$tempstat" "$statlist"; then
        sort -k 4,4n -k 5,5n -k 6,6n -k 7,7n -k 8,8n -o "$statlist" "$statlist"
        cp "$statlist" "$tempstat"
    fi
}

archive () {
    find "$new" -type f > "$templist"
    newn="$(awk 'END{print NR}' "$templist")"
    test "$newn" -eq 0 && return 1
    while read -r file; do
        filename="${file##*/}"
        mv "$file" "${cur}/${filename%:*}:2,S"
    done < "$templist"
}

loop () {
    i=0; j=0
    while read -r line; do
        i="$((i+1))"
        if [ "$cmd" = 'z' ]; then
            true
        elif [ "$i" -le "$upper" ]; then
            if [ "$i" -ge "$lower" ]; then
                j="$((j+1))"
            else
                continue
            fi
        else
            break
        fi
        "$1" "$line"
    done < "$statlist"
}

term () {
    cols="$(tput cols)"
    rows="$(tput lines)"
    rows="$((rows-3))"
    test "$rows" -lt 10 && rows=10
    upper="$((cursor+rows-6))"
    test "$upper" -gt "$n" && upper="$n"
    lower="$((upper-rows+1))"
    if [ "$lower" -lt 1 ]; then
        upper="$((upper-lower+1))"
        lower=1
    fi
}

field () {
    printf '%s\n' "$head" | awk -v p="^$1: " '$0~p{print;exit}' | cut -d' ' -f2-
}

brief () {
    set -- $1
    file="$1"; lines="$2"; bytes="$3"; date="$5/$6/$4 $7:$8"
    head="$(awk 'BEGIN{RS=""}NR==1{print;exit}' "$file")"
    case "$dir" in
        "$tmp"|"$record")
            name="$(field 'To')" ;;
        *)
            name="$(field 'From' | tr -d \")"; name="${name% <*}" ;;
    esac
    name="$(printf '%s\n' "$name" | cut -c-22)"
    subj="$(field 'Subject')"
    if [ "$lines" -lt 1000 ] && [ "$bytes" -lt 10000 ]; then
        size="$lines/$bytes"
    else
        size="$(du -h "$file" | cut -f1 )"
    fi
    pre='  '
    if [ "$cmd" = 'z' ]; then
        unset pre
    elif [ "$i" -eq "$cursor" ]; then
        pre='->'
    fi
    c="${#upper}"; wdth="$((cols-54-c))"
    set -- "$pre" "$c" "$i" "$name" "$date" "$size" "$wdth" "$subj"
    printf '%s%*s %-22s %s %-8s "%.*s"\n' "$@"
}

headline () {
    unset match sort
    test "$filter" && match="${inverse:+ *not*} matching \"$pat\""
    test "$bysize" && sort=', sorted by size'
    case "$n" in
        0)
            printf '%s: no messages%s%s\n' "$dir" "${match:-}" "${sort:-}"
            cursor=1; return ;;
        1)
            printf '%s: %s messages%s%s\n' "$dir" "$n" "${match:-}"\
                "${sort:-}" ;;
        *)
            printf '%s: %s messages%s%s\n' "$dir" "$n" "${match:-}"\
                "${sort:-}" ;;
    esac
}

list () {
    update; term
    if [ "$cmd" = 'z' ]; then
        wdth="$((wdth+2))"
        headline; loop 'brief' | pager
    else
        headline; loop 'brief'
    fi
}

part () {
    file="$(awk -v i="$cursor" 'NR==i{print $1;exit}' "$statlist")"
    head="$(awk 'BEGIN{RS=""}NR==1{print;exit}' "$file")"
    body="$(awk '/^$/?i++:i' "$file")"
}

retain () {
    date="Date: $(field 'Date')"
    addr="From: $(field 'From')"
    to="To: $(field 'To')"
    cc="$(field 'Cc')"
    subj="Subject: $(field 'Subject')"
    if [ -n "$cc" ]; then
        set -- "$date" "$addr" "$to" "Cc: $cc" "$subj"
    else
        set -- "$date" "$addr" "$to" "$subj"
    fi
    retain="$(printf '%s\n' "$@")"
}

page () {
    if [ "$n" -eq 0 ]; then
        printf 'EOF\n'
        return 1
    fi
    cursor="${cmd:-"$cursor"}"
    part
    retain
    tput clear
    printf '%s\n' "$retain" '' "$body" | pager
}

int () {
    if ([ "$1" -gt 0 ] && [ "$1" -le "$n" ]) 2>'/dev/null'; then
        return 0
    fi
    return 1
}

valid () {
    arg="${cmd#?}"
    if [ -n "$arg" ]; then
        lower="${arg%-*}"
        upper="${arg#*-}"
        if [ "$arg" = '*' ]; then
            lower=1
            upper="$n"
        elif [ "$arg" = '-.' ]; then
            lower=1
            upper="$cursor"
        elif [ "$arg" = '.-' ]; then
            lower="$cursor"
            upper="$n"
        elif [ "$arg" = ',' ]; then
            lower="$n"
            upper="$n"
        elif [ "$arg" = '-' ]; then
            printf '?\n'
            return 1
        else
            test -z "$upper" && upper="$n"
            test -z "$lower" && lower=1
        fi
    elif [ "$n" -eq 0 ]; then
        printf 'EOF\n'
        return 1
    else
        lower="$cursor"
        upper="$cursor"
        return 0
    fi
    if int "$lower" && int "$upper"; then
        case "$cmd" in
            'd'*|'y'*|'~'*)
                if [ ! "$upper" -ge "$lower" ]; then
                    printf 'invalid range\n'
                    return 1
                fi ;;
            *)
                if [ "$lower" -ne "$upper" ]; then
                    printf 'this command supports no ranges\n'
                    return 1
                else
                    cursor="$lower"
                fi ;;
        esac
    else
        printf '?\n'
        return 1
    fi
}

header () {
    valid || return 1
    part
    tput clear
    printf '%s\n' "$head" | pager
}

pipe () {
    valid || return 1
    part
    tput clear
    test "$args" || args="$pipeto"
    printf '%s\n' "$body" | sh -c "$args" | pager
}

cpdo () {
    file="${1%% *}"
    filename="${file##*/}"
    cp "$file" "$args/${filename%:*}:2,S"
}

copy () {
    valid || return 1
    case "$args" in
        '/'*)
            true ;;
        ''|'~')
            args="$HOME" ;;
        'i')
            args="$new" ;;
        'c')
            args="$cur" ;;
        't')
            args="$tmp" ;;
        'o')
            if [ -n "$record" ]; then
                args="$record"
            else
                args="$HOME/$args/o"
            fi ;;
        *)
            args="$HOME/$args" ;;
    esac
    if [ ! -d "$args" ]; then
        printf '%s does not exist, create it? [Y/n]: ' "$args"
        read -r mkd
        case "$mkd" in
            ''|'Y'|'y')
                mkdir -p "$args" ;;
            *)
                printf 'aborting\n'
                return 1 ;;
        esac
    fi
    loop 'cpdo'
    count="$((upper-lower+1))"
    if [ "$count" -eq 1 ]; then
        printf 'one message copied to %s\n' "$args"
    else
        printf '%s messages copied to %s\n' "$count" "$args"
    fi
}

printfile () {
    printf '%s\n' "${1%% *}" >> "$templist"
}

delete () {
    valid || return 1
    count="$((upper-lower+1))"
    true > "$templist"
    loop 'printfile'
    while read -r file; do
        filename="${file##*/}"
        printf '/%s/d\n%s\n' "$filename" 'wq' | ed -s "$filelist"
        printf '/%s/d\n%s\n' "$filename" 'wq' | ed -s "$statlist"
        if [ "$dir" = "$del" ]; then
            rm "$file"
        else
            mv "$file" "${del}/${filename%:*}:2,T"
        fi
    done < "$templist"
    cp "$statlist" "$tempstat"
    n="$(awk 'END{print NR}' "$filelist")"
    if [ "$lower" -lt "$cursor" ]; then
        if [ "$upper" -ge "$cursor" ]; then
            cursor="$lower"
        else
            cursor="$((cursor-upper+lower-1))"
        fi
    fi
    test "$cursor" -gt "$n" && cursor="$n"
    test "$cursor" -lt 1 && cursor=1
    if [ "$count" -gt 1 ]; then
        printf '%s messages deleted\n' "$count"
    fi
}

aliases () {
    test -f "$aliases" || return 1
    set -- $(printf '%s\n' "$to" | tr ',' ' ')
    for addr in "$@"; do
        local="${addr%@*}"
        if [ "$addr" = "$local" ]; then
            a="$(awk -v p="^$addr: " '$0~p{print;exit}' "$aliases" |
            cut -d':' -f2- | tr -d ' ')"
        else
            unset a
        fi
        shift; set -- $@ "${a:-"$addr"}"
    done
    to="$(printf '%s\n' '0a' "$*" '.' 'g/ /s//,/' 'g/,/s//, /' 'p' | ed -s)"
}

parsemsg () {
    mid="$(field 'Message-ID')"
    addr="$(field 'From')"
    subj="$(field 'Subject')"
    replyto="$(field 'Reply-To')"
    to="${replyto:-"$addr"}"
    to="${to#*<}"; to="${to%>*}"
    subj="Re: ${subj#Re: *}"
    ref="$(printf '%s\n' "$head" | awk '/:/{i=0}/^References:/{i=1}i')"
    irt="$(printf '%s\n' "$head" | awk '/:/{i=0}/^In-Reply-To:/{i=1}i')"
    irtl="$(printf '%s\n' "$irt" | awk 'END{print NR}')"
}

toed () {
    printf '%s\n' "$@" | ed -s "$msg"
}

hdrs () {
    h1="Message-ID: <${msg##*/}>"
    h2="Date: $(date '+%a, %d %b %Y %T %z')"
    h3="From: $from"
    h4="To: $to"
    h5="Subject: $subj"
    toed '0a' "$h1" "$h2" "$h3" "$h4" "$h5" '' '.' 'w'
}

reply () {
    valid || return 1
    part
    parsemsg
    printf 'To [%s]: ' "$to"
    read -r newto
    printf 'Subject [%s]: ' "$subj"
    read -r newsubj
    subj="${newsubj:-"$subj"}"
    to="${newto:-"$to"}"
    msg="${tmp}/$(date '+%s')-edmail|$user@$domain"
    printf '%s\n' "$body" > "$msg"
    toed 'g/^/s//> /' '0a' "$addr wrote:" '' '.' 'w'
    editor "$msg"
    test -f "$sign" && cat "$sign" >> "$msg"
    aliases
    hdrs
    if [ -n "$ref" ]; then
        toed '0a' "$ref" " $mid" '.' 'w'
    elif [ -n "$irt" ] && [ "$irtl" -eq 1 ]; then
        toed '0a' "References: ${irt#* }" " $mid" '.' 'w'
    fi
    if [ -n "$mid" ]; then
        toed '0a' "In-Reply-To: $mid" '.' 'w'
    fi
}

edit () {
    if [ "${dir##*/}" != 'tmp' ] && [ "$cmd" != '#' ]; then
        printf 'will not comply outside tmp/\n'
        return 1
    fi
    valid || return 1
    part
    if [ "$cmd" = '#' ]; then
        editor "$file"
        return 0
    fi
    to="$(field 'To')"
    subj="$(field 'Subject')"
    printf 'To [%s]: ' "$to"
    read -r newto
    printf 'Subject [%s]: ' "$subj"
    read -r newsubj
    subj="${newsubj:-"$subj"}"
    to="${newto:-"$to"}"
    msg="${tmp}/$(date '+%s')-edmail|$user@$domain"
    printf '%s\n' "$body" > "$msg"
    editor "$msg"
    aliases
    hdrs
    rm "$file"
    update
}

compose () {
    if [ -n "$args" ]; then
        to="$args"
    else
        printf 'To: '
        read -r to
    fi
    printf 'Subject: '
    read -r subj
    msg="${tmp}/$(date '+%s')-edmail|$user@$domain"
    editor "$msg"
    if [ -f "$msg" ]; then
        test -f "$sign" && cat "$sign" >> "$msg"
        aliases
        hdrs
    else
        printf 'nothing written\n'
    fi
}

send () {
    if [ -z "$(find "$tmp" -type f)" ]; then
        printf '%s contains no messages\n' "$tmp"
        return 1
    fi
    i=0; j=0
    for file in "$tmp"/*; do
        j="$((j+1))"
        addr="$(awk '/^To: /{print;exit}' "$file" | cut -d' ' -f2-)"
        if sendmail "$addr" < "$file"; then
            i="$((i+1))"
            if [ -n "$record" ]; then
                mv "$file" "$record"
            else
                rm "$file"
            fi
        elif [ "$sendmail" != 'null' ]; then
            printf 'edmail: could not send %s\n\n' "$file"
        fi
    done
    if [ "$i" -eq 0 ]; then
        printf 'no messages sent\n'
    elif [ "$j" -eq 1 ]; then
        printf '%s/%s message successfully sent\n' "$i" "$j"
    else
        printf '%s/%s messages successfully sent\n' "$i" "$j"
    fi
    test "${dir##*/}" = 'tmp' && update
}

seterr () {
    if [ "$args" = '-' ]; then
        printf 'cannot be unset\n'
        return 1
    fi
}

setopts () {
    if [ -z "$args" ]; then
        printf '%10s = %s\n'\
            fetchmail "$fetchmail"\
            sendmail "$sendmail"\
            folder "$maildir"\
            record "${record:-null}"\
            from "$from"\
            aliases "${aliases:-null}"\
            signature "${sign:-null}"\
            EDITOR "$editor"\
            PAGER "$pager"\
            cmd "${pipeto:-null}"\
            script "${script:-null}"
        return 0
    fi
    set -- $args
    option="$1"; shift
    if [ -z "$1" ]; then
        printf '?\n'
        return 1
    fi
    args="$*"
    case "$option" in
        'fe'|'fetchmail')
            seterr || return 1
            exe "$args" && fetchmail="$args"
            fetchmail () { "$fetchmail"; } ;;
        'se'|'sendmail')
            seterr || return 1
            exe "$args" && sendmail="$args"
            sendmail () { "$sendmail" "$@" || return 1; } ;;
        'fo'|'folder')
            seterr || return 1
            if [ -d "$args" ]; then
                maildir="$args"
            else
                printf '%s %s\n' "$args is not a directory"
            fi ;;
        're'|'record')
            case "$args" in
                '+'*) args="$maildir/${args#+}" ;;
            esac
            if [ -d "$args" ]; then
                record="$args"
            elif [ "$args" = '-' ]; then
                unset record
            else
                printf '%s %s\n' "$args is not a directory"
            fi ;;
        'fr'|'from')
            seterr || return 1
            from="$args" ;;
        'al'|'aliases')
            if [ -f "$args" ]; then
                aliases="$args"
            elif [ "$args" = '-' ]; then
                unset aliases
            else
                printf '%s is not a file\n' "$args"
            fi ;;
        'si'|'signature')
            if [ -f "$args" ]; then
                sign="$args"
            elif [ "$args" = '-' ]; then
                unset sign
            else
                printf '%s is not a file\n' "$args"
            fi ;;
        'ed'|'ED'|'EDITOR'|'editor')
            seterr || return 1
            exe "$args" && editor="$args"
            editor () { "$editor" "$@"; } ;;
        'pa'|'PA'|'PAGER'|'pager')
            seterr || return 1
            exe "$args" && pager="$args"
            pager () { "$pager" "$@"; } ;;
        'cm'|'cmd')
            if [ "$args" = '-' ]; then
                unset pipeto
            else
                pipeto="$args"
            fi ;;
        'sc'|'script')
            if [ "$args" = '-' ]; then
                unset script
                script () { null; }
            else
                exe "$args" && script="$args"
                script () { "$script" "$@"; }
            fi ;;
        *)
            printf 'no such option\n' ;;
    esac
}

parserc () {
    val="$(printf '%s\n' 'g/ *$/s///' "/^set $1=" | ed -s "$mailrc"\
        2>'/dev/null' | cut -d'=' -f2-)"
    case "$val" in
        '"'*|"'"*)
            val="${val#?}"
            val="${val%?}" ;;
    esac
    printf '%s\n' "$val"
}

exe () {
    for bin in "$@"; do
        if ! command -v "$bin" 1>'/dev/null'; then
            printf '%s is not executable\n' "$bin"
            return 1
        fi
    done
}

null () {
    printf 'no executable set\n'
    return 1
}

bins () {
    fetchmail="${fetchmail:-null}"
    sendmail="${sendmail:-null}"
    editor="${editor:-ed}"
    pager="${pager:-more}"
    script="${script:-null}"
    set -- "$fetchmail" "$sendmail" "$editor" "$pager" "$script"
    exe "$@" || return 1
    fetchmail () { "$fetchmail"; }
    sendmail () { "$sendmail" "$@" || return 1; }
    editor () { "$editor" "$@"; }
    pager () { "$pager" "$@"; }
    script () { "$script" "$@"; }
}

statfold () {
    for fold in "$@"; do
        if [ ! -d "$fold" ]; then
            if [ -z "$said" ]; then
                printf 'could not find the following, required, directories:\n'
                said=0
            fi
            printf ' %s\n' "$fold"
            notfound=0
        fi
    done
    test "$notfound" && return 0
}

define () {
    new="$maildir/new"
    cur="$maildir/cur"
    tmp="$maildir/tmp"
    del="$maildir/.del"
    user="${from##*<}"
    user="${user%@*}"
    domain="${from##*@}"
    domain="${domain%>*}"
}

init () {
    test "$1" && mailrc="$1"
    if [ ! -f "$mailrc" ]; then
        printf 'could not find %s\n' "$mailrc"
        return 1
    fi
    fetchmail="$(parserc 'fetchmail')"
    sendmail="$(parserc 'sendmail')"
    maildir="$(parserc 'folder')"
    record="$(parserc 'record')"
    from="$(parserc 'from')"
    aliases="$(parserc 'aliases')"
    sign="$(parserc 'signature')"
    editor="$(parserc 'EDITOR')"
    pager="$(parserc 'PAGER')"
    pipeto="$(parserc 'cmd')"
    script="$(parserc 'script')"
    bins || return 1
    if [ -z "$maildir" ]; then
        maildir="$HOME/.maildir"
    fi
    define
    set -- "$new" "$cur" "$tmp" "$del"
    if [ -n "$record" ]; then
        case "$record" in
            '+'*) record="$maildir/${record#+}" ;;
        esac
        set -- "$@" "$record"
    fi
    if [ -z "$from" ]; then
        printf "set the string option 'from' in %s\\n" "$mailrc"
        return 1
    elif statfold "$@"; then
        printf 'create them? [Y/n]: '
        read -r mkd
        case "$mkd" in
            ''|'Y'|'y')
                if mkdir -p "$@"; then
                    printf '#\n' > "$templist"
                    return 0
                fi ;;
            *)
                printf 'exiting\n' ;;
        esac
    else
        printf '#\n' > "$templist"
        return 0
    fi
    return 1
}

serv () {
    case "$cmd" in
        '')
            page ;;
        ',')
            cmd="$n"
            page ;;
        'f'|'+')
            if [ "$cursor" -lt "$n" ]; then
                cursor="$((cursor+1))"
                cmd="$cursor"
                page
            else
                printf 'EOF\n'
            fi ;;
        'b'|'-')
            if [ "$cursor" -gt 1 ]; then
                cursor="$((cursor-1))"
                cmd="$cursor"
                page
            else
                printf 'BOF\n'
            fi ;;
        'l'|'.')
            list ;;
        'z')
            list ;;
        'g')
            cursor=1
            list ;;
        'g'*)
            valid || return 1
            list ;;
        'G')
            cursor="$n"
            list ;;
        'k')
            k="$((cursor-20))"
            if [ "$k" -le 1 ]; then
                cursor=1
            else
                cursor="$k"
            fi
            list ;;
        'j')
            j="$((cursor+20))"
            if [ "$j" -le "$n" ]; then
                cursor="$j"
            fi
            list ;;
        'i')
            dir="$new"
            cursor=1
            list ;;
        'c')
            dir="$cur"
            cursor=1
            list ;;
        't')
            dir="$tmp"
            cursor=1
            list ;;
        '%')
            dir="$del"
            cursor=1
            list ;;
        'o')
            if [ -n "$record" ]; then
                dir="$record"
                cursor=1
                list
            else
                printf '?\n'
            fi ;;
        'p'*)
            if [ -n "$pipeto" ]; then
                pipe
            else
                printf '?\n'
            fi ;;
        'm')
            compose ;;
        'h'*)
            header ;;
        'd'*)
            delete ;;
        'r'*)
            reply ;;
        'y'*)
            copy ;;
        'e'*|'#'*)
            edit ;;
        's')
            setopts ;;
        'S')
            oldrc="$mailrc"
            init "$args" || init "$oldrc" ;;
        'w')
            send ;;
        'n')
            fetchmail
            test "$dir" = "$new" && update ;;
        'v')
            printf 'edmail version 0.3.2\n' ;;
        'a')
            if archive; then
                if [ "$newn" -eq 1 ]; then
                    printf '1 new message moved to %s\n' "$cur"
                else
                    printf '%s new messages moved to %s\n' "$newn" "$cur"
                fi
            else
                printf 'no messages to archive\n'
            fi
            test "$dir" = "$new" && update ;;
        'q')
            find "$del" -type f -exec rm {} +
            archive; exit 0 ;;
        'x')
            exit 0 ;;
        'u')
            if [ -z "$bysize" ]; then
                bysize=0
            else
                unset bysize
            fi
            list ;;
        '/')
            if [ -z "$filter" ]; then
                filter=0
            else
                unset filter
            fi
            list ;;
        '/!'*)
            pat="${cmd#??}${args:+" $args"}"
            inverse=0
            filter=0
            cursor=1
            list ;;
        '/'*)
            pat="${cmd#?}${args:+" $args"}"
            unset inverse
            filter=0
            cursor=1
            list ;;
        '@')
            if [ -f "$aliases" ]; then
                pager "$aliases"
            else
                printf '?\n'
            fi ;;
        "'"*)
            valid || return 1
            file="$(awk -v i="$cursor" 'NR==i{print $1;exit}' "$statlist")"
            script "$file" "$args" ;;
        '~'*)
            valid || return 1
            awk -v a="$lower" -v b="$upper" 'NR>=a&&NR<=b{print $1}'\
                "$statlist" ;;
        ';')
            tput clear ;;
        '?')
            man edmail ;;
        '!'|'$')
            sh ;;
        '!'*)
            sh -c "${cmd#?} $args" ;;
        *)
            if int "$cmd"; then
                page
            else
                printf '?\n'
            fi ;;
    esac
}

main () {
    command -v mktemp 1>'/dev/null' || exit 1
    tempdir="$(mktemp -dt edmail.XXXXXX)"
    filelist="$tempdir/filelist"
    templist="$tempdir/templist"
    statlist="$tempdir/statlist"
    tempstat="$tempdir/tempstat"
    trap "rm -r $tempdir; exit" 0 1
    trap "printf '\n'; continue" 2
    mailrc="$HOME/.mailrc"
    test "$1" && mailrc="$1"
    init || exit 1
    cmd='i'; serv
    while true; do
        printf '& '
        read -r cmd args
        serv
    done
}

main "$@"
